﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Browser;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Collections.ObjectModel;

namespace SuperDeepZoom
{
    //Template parts define the names and types of objects this control exptects
    //to see in it's template. They are searched for in OnApplyTemplate()
    //This follows the pattern called the Parts and States model

    [TemplatePart(Name = "RootMultiScaleImage", Type = typeof(MultiScaleImage))]
    [TemplatePart(Name = "FlyThroughOverlay", Type = typeof(FlyThroughOverlay))]
    [TemplatePart(Name = "SpecialImageOverlay", Type = typeof(SpecialImageOverlay))]

    public class SuperDeepZoom : Control, INotifyPropertyChanged
    {
        //Bind the source of the deep zoom image.
        public static readonly DependencyProperty DeepZoomSourceProperty;

        //Set the current filter ids, this allows the caller to force a filter on the msi
        public static readonly DependencyProperty FilterIdsProperty;

        //Allows the consumer to control the flythrough visibility
        public static readonly DependencyProperty FlyThroughOverlayEnabledProperty;


        //Allows the consumer to control the image overlay visibiliy
        public static readonly DependencyProperty SpecialImageOverlayEnabledProperty;

        //Passes in a JS function that sould be called when ever the viewed image changes
        //Function needs to be on the global JS object and should take one parameter (the image ID)
        //Great for ad rotation
        public static readonly DependencyProperty ImageViewedJavaScriptFunctionProperty;

        //Time for automatic transition between images in slideshows and flythroughs
        //Should be an integer in milliseconds
        public static readonly DependencyProperty SlideshowTimeoutProperty;

        //Each image in the msi can have it's own context which can be used
        //to pass in metdata information like title, url or view count
        //This data is then served up when required, like a detail view or
        //special overlay operation for databinding in the UI
        public static readonly DependencyProperty ImageContextDataProperty;

        //The member variable for the multiscale image.
        //You should not access this directly, you should instead go through the 
        //RootMultiScaleImage property.
        //This comes from the template
        MultiScaleImage msi;

        //This control comes from teh template also.
        //This control shows the overlay of a flythrough
        FlyThroughOverlay flyThroughOverlay;

        SpecialImageOverlay specialImageOverlay;

        //The image arranger is responsible to handling deep zoom re-arrangement
        //metadata, zooming, image selection, deep zoom to real coordinate mapping etc.
        ImageArranger arranger;

        //The mouse helper assist with mouse movements. I creates drag and click behavior.
        MouseMovementHelper mouseHelper;

        //This class manages the slide show (navigation between images squentially)
        Slideshow slideShow;

        //The filter is responsible for showing and hiding images based on input
        //for example filter by tag.
        FilterHelper filter;

        //Records and plays back fly throughs. Encodes them in to URL's for 
        //copy and paste to other users
        FlyThroughManager flyThroughManager;




        //Notifies the consumer that metadata has finished loading from the server
        public event EventHandler MetadataLoaded;


        public SuperDeepZoom()
        {
            DefaultStyleKey = typeof(SuperDeepZoom);

            if (HtmlPage.IsEnabled)
            {
                Application.Current.Host.Content.Resized += (object sender, EventArgs e) =>
                {
                    if (RootMultiScaleImage != null && RootMultiScaleImage.ActualWidth > 0 && arranger != null)
                    {
                        arranger.ArrangeImages();
                    }
                };

                Application.Current.Host.Content.FullScreenChanged += (object sender, EventArgs e) =>
                {
                    if (RootMultiScaleImage != null && arranger != null)
                    {
                        arranger.ArrangeImages();
                    }
                };


                //Add key handling so the user can navigate around to MSI            
                this.KeyDown += (s, e) =>
                {
                    Point p = msi.ElementToLogicalPoint(new Point((msi.Width / 2), ((msi.Width / msi.AspectRatio) / 2)));
                    switch (e.Key)
                    {
                        case Key.Q:
                            arranger.ZoomInDefault();
                            break;
                        case Key.E:
                            arranger.ZoomOutDefault();
                            break;
                        case Key.A:
                            msi.ViewportOrigin = new Point(msi.ViewportOrigin.X - 0.02, msi.ViewportOrigin.Y);
                            break;
                        case Key.D:
                            msi.ViewportOrigin = new Point(msi.ViewportOrigin.X + 0.02, msi.ViewportOrigin.Y);
                            break;
                        case Key.W:
                            msi.ViewportOrigin = new Point(msi.ViewportOrigin.X, msi.ViewportOrigin.Y - 0.02);
                            break;
                        case Key.S:
                            msi.ViewportOrigin = new Point(msi.ViewportOrigin.X, msi.ViewportOrigin.Y + 0.02);
                            break;
                    }
                };
            }
        }

        /// <summary>
        /// Show all the images in the msi. Clears any filters set
        /// </summary>
        public void ShowAllImages()
        {
            arranger.ArrangeImages(filter.AllImages(), null);
        }

        /// <summary>
        /// Only show images which match the input criteria
        /// This is a little hardcoded - future revisions may allow for a softer approach to filtering
        /// </summary>      
        public void FilterImages(List<int> imageIds)
        {
            if (filter != null)
            {
                List<MultiScaleSubImage> filterImages = filter.FilterImages(imageIds);

                if (filterImages.Count == 0)
                {
                    arranger.ArrangeImages(filter.AllImages(), null);
                }
                else
                {
                    arranger.ArrangeImages(filterImages, filter.ImagesToHide(filterImages));
                }
            }
        }

        /// <summary>
        /// Whenever filters change, this will cause the filters to be applied.
        /// This is called from the FilterIdsProperty dependency property changed event.
        /// </summary>
        protected void initFilterIds()
        {
            if (FilterIds != null)
            {
                FilterImages(FilterIds);
            }
            else
            {
                ShowAllImages();
            }
        }

        /// <summary>
        /// Start a slideshow
        /// </summary>
        /// <param name="delay">They delay between slide transitions</param>
        public void StartSlideshow()
        {
            TimeSpan delay = TimeSpan.FromMilliseconds(SlideshowTimeout);
            slideShow.StartSlideShow(delay, arranger.ImagesToShow);
        }

        /// <summary>
        /// Stop a running slideshow
        /// </summary>
        public void StopSlideshow()
        {
            if (slideShow != null)
            {
                slideShow.StopSlideShow();
            }
        }

        /// <summary>
        /// Indicates if a slideshow is currently running.
        /// This does not include FlyThroughs
        /// </summary>
        public bool SlideshowRunning
        {
            get
            {
                if (slideShow != null)
                {
                    return slideShow.IsRunning;
                }
                else
                {
                    return false;
                }
            }
        }

        /// <summary>
        /// Forces next slide whether slideshow is running or not
        /// Will reset the next slide count down timer if slideshow is running
        /// </summary>
        public void NextSlide()
        {
            if (slideShow != null)
            {
                slideShow.NextSlide();
            }
        }

        /// <summary>
        /// Forces previous slide whether slideshow is running or not
        /// Will reset the next slide count down timer if slideshow is running
        /// </summary>
        public void PrevSlide()
        {
            if (slideShow != null)
            {
                slideShow.PrevSlide();
            }
        }

        /// <summary>
        /// Switches Flythrough overlay to show Hash URL based passed in overlay
        /// or the overlay from the current user's session.        
        /// </summary>        
        public void SwitchOverlaySource(FlyThroughSource source)
        {
            if (flyThroughOverlay != null && flyThroughOverlay.FlyThroughOverlayManager != null)
            {
                flyThroughOverlay.FlyThroughOverlayManager.SwitchOverlaySource(source);
            }
        }

        /// <summary>
        /// Allow hiding of overlay without breaking binding on the FlyThroughOverlayEnabled property
        /// Assists when being used from code (as opposed to xaml)
        /// </summary>
        public void HideFlyThroughOverlay()
        {
            if (FlyThroughOverlay != null)
            {
                FlyThroughOverlayEnabled = false;
                FlyThroughOverlay.ToggleOverlayEnabled(false);
            }
        }

        /// <summary>
        /// Runs the active flythrough, if there is one
        /// </summary>
        public void PlayFlyThrough(TimeSpan delay)
        {
            if (flyThroughManager != null && flyThroughOverlay.FlyThroughOverlayManager != null)
            {
                List<MultiScaleSubImage> flyThroughImages = flyThroughOverlay.FlyThroughOverlayManager.CurrentFlyThoughImages;
                if (flyThroughImages != null)
                {
                    slideShow.StartSlideShow(delay, flyThroughImages);
                }
            }
        }

        public void ZoomOut()
        {
            arranger.ZoomOutDefault();
        }

        public void ZoomIn()
        {
            arranger.ZoomInDefault();
        }

        /// <summary>
        /// initialises the control and prepares it for display.
        /// This is called on load and whenever the DeepZoomSource property changes
        /// </summary>
        protected void init()
        {
            //Note how this accesses the DeepZoomSource CLR property
            //and not the dependency property directly.
            //This is to make coding easier.

            if (HtmlPage.IsEnabled)
            {

                if (!string.IsNullOrEmpty(DeepZoomSource) && RootMultiScaleImage != null)
                {
                    var dzSource = DeepZoomSource;

                    Uri dzSourceUri;

                    //If the url starts with . (as in ../) or / (as in /somthing.xml) then it is relative, otherwise let it load from anywhere             
                    if (dzSource.StartsWith("."))
                    {
                        dzSourceUri = new Uri(Application.Current.Host.Source, new Uri(dzSource, UriKind.Relative));
                    }
                    else
                    {
                        dzSourceUri = new Uri(dzSource, UriKind.RelativeOrAbsolute);
                    }

                    //Load the new source in to the MSI
                    RootMultiScaleImage.Source = new DeepZoomImageTileSource(dzSourceUri);

                    msiChanged();

                    filter = new FilterHelper(msi);

                    mouseHelper = new MouseMovementHelper(this, arranger, RootMultiScaleImage);

                    toggleSpecialImageOverlayEnabled();

                }
            }
        }

        /// <summary>
        /// When ever the MSI is changed (it's source is altered) this will re-initialise all the 
        /// various management classes so they will now deal with the new source
        /// </summary>
        void msiChanged()
        {
            //Ensure that if the arranger has already been initiated to remove any events
            if (arranger != null)
            {
                arranger.ViewedImageChanged -= new EventHandler(arranger_ViewedImageChanged);
                arranger.Dispose();
            }

            arranger = new ImageArranger(RootMultiScaleImage);

            arranger.ViewedImageChanged += new EventHandler(arranger_ViewedImageChanged);

            //Init the fly through manager to assist with passed in Fly throughs and also
            //fly through recording during user navigation around the MSI
            if (flyThroughManager != null)
            {
                flyThroughManager.HashChanged -= new EventHandler(flyThroughManager_HashChanged);
                flyThroughManager.Dispose();
            }

            flyThroughManager = new FlyThroughManager(arranger, this);
            flyThroughManager.HashChanged += new EventHandler(flyThroughManager_HashChanged);


            onPropertyChanged("HasUrlFlyThrough");


            if (FlyThroughOverlay != null)
            {
                FlyThroughOverlay.MultiScaleImage = msi;
                FlyThroughOverlay.ImageArranger = arranger;
                FlyThroughOverlay.FlyThroughManager = flyThroughManager;
            }

            //The special image overlay is an overlay that has items for each item in the MSI
            //This can be used to do some sweeeeeeeeet stuff.
            if (SpecialImageOverlay != null)
            {
                SpecialImageOverlay.MultiScaleImage = msi;
                SpecialImageOverlay.ImageArranger = arranger;
                prepSpecialOverlay();
            }

            //The slideshow class implements slideshow behavior for the MSI.
            slideShow = new Slideshow(msi, arranger);
            slideShow.SlideShowRunStateChanged += (s, e) =>
            {
                onPropertyChanged("SlideshowRunning");
            };
        }

        /// <summary>
        /// Passes through FlyThroughOverlay enabled state
        /// </summary>
        protected void toggleFlyThroughEnabled()
        {
            if (FlyThroughOverlay != null)
            {
                FlyThroughOverlay.ToggleOverlayEnabled(FlyThroughOverlayEnabled);
            }
        }

        /// <summary>
        /// Show/hide the special image overlay
        /// </summary>
        protected void toggleSpecialImageOverlayEnabled()
        {
            if (SpecialImageOverlay != null)
            {
                SpecialImageOverlay.IsVisible = SpecialImageOverlayEnabled;
            }
        }

        /// <summary>
        /// Pass the context data to the image overlay. Image overlay is a ItemsControl, meaning it's fully templatable
        /// </summary>
        protected void prepSpecialOverlay()
        {
            if (SpecialImageOverlay != null && ImageContextData != null)
            {
                SpecialImageOverlay.ItemsSource = ImageContextData;
            }
        }

        /// <summary>
        /// If a image data object implements the ISpecialImageOverlayData interface it can be used
        /// to pause and resume a slide show to play a video etc.
        /// </summary>        
        protected void contextDataEvents(Dictionary<int, object> oldData, Dictionary<int, object> newData)
        {
            if (oldData != null)
            {
                foreach (var obj in oldData.Values)
                {
                    var iSpecial = obj as ISpecialImageOverlayData;

                    if (iSpecial != null)
                    {
                        iSpecial.Pause -= new EventHandler(iSpecial_Pause);
                        iSpecial.Resume -= new EventHandler(iSpecial_Resume);
                    }
                }
            }

            foreach (var obj in newData.Values)
            {
                var iSpecial = obj as ISpecialImageOverlayData;

                if (iSpecial != null)
                {
                    iSpecial.Pause += new EventHandler(iSpecial_Pause);
                    iSpecial.Resume += new EventHandler(iSpecial_Resume);
                }
            }
        }
        
        /// <summary>
        /// Perform the callback to JS when an image changes.
        /// </summary>
        void jsCallback()
        {
            if (!slideShow.IsRunning)
            {
                if (ViewedImage != null && ViewedImage.HasValue)
                {
                    if (!string.IsNullOrEmpty(ImageViewedJavaScriptFunction))
                    {
                        HtmlPage.Window.Invoke(ImageViewedJavaScriptFunction, ViewedImage.Value);
                    }
                }
            }
        }

        /// <summary>
        /// OnApplyTemplate is the first chance this control gets to inspect it's template
        /// Controls are searched for and added in to member variables for later usage
        /// </summary>
        public override void OnApplyTemplate()
        {
            //This will return null if it cannot be found or is the wrong type
            var rootMsi = GetTemplateChild("RootMultiScaleImage") as MultiScaleImage;

            //pass the new MultiScaleImage control from the template in to the property
            //this property will add (and remove if needed) event handlers etc.
            //When working with template controls you should not access their local member variable
            //directly as the property might be performing extra (encapsulated) work for you.
            RootMultiScaleImage = rootMsi;
            RootMultiScaleImage.UseSprings = false;

            //Get out the FlyThroughOverlay. If not present this control will not attempt
            //to show a fly through overlay. Good template control design dictates that 
            //you should never depend on a particular control to be present in the template.
            FlyThroughOverlay = GetTemplateChild("FlyThroughOverlay") as FlyThroughOverlay;

            //An overlay that applies a templated control over each image on the msi
            SpecialImageOverlay = GetTemplateChild("SpecialImageOverlay") as SpecialImageOverlay;

            //Cause the first init, everything is loaded so the system is ready to roll
            init();
        }

        #region EventHandlers

        /// <summary>
        /// Allows the data context objects to pause the slide show (to play a video or something)
        /// </summary>       
        void iSpecial_Resume(object sender, EventArgs e)
        {
            slideShow.Resume();
        }

        /// <summary>
        /// Allows the data context objects to pause the slide show (to play a video or something)
        /// </summary>       
        void iSpecial_Pause(object sender, EventArgs e)
        {
            slideShow.Pause();
        }


        void msi_MotionFinished(object sender, RoutedEventArgs e)
        {
            if (msi.UseSprings == false)
            {
                ShowAllImages();
                msi.UseSprings = true;
            }
        }

        /// <summary>
        /// Notify when metadata is ready for consumption
        /// </summary>       
        void metadataManager_DataLoaded(object sender, EventArgs e)
        {
            onPropertyChanged("SubImageMetadata");

            onPropertyChanged("AllTags");

            if (MetadataLoaded != null)
            {
                MetadataLoaded(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Notify that the viewed image has changed
        /// </summary>        
        void arranger_ViewedImageChanged(object sender, EventArgs e)
        {
            jsCallback();
            onPropertyChanged("FlyThroughHash");
            onPropertyChanged("ViewedImage");
            onPropertyChanged("CurrentImageContext");
        }

        void flyThroughManager_HashChanged(object sender, EventArgs e)
        {
            onPropertyChanged("FlyThroughHash");
        }

        #endregion


        #region TemplateControlProperties

        /// <summary>
        /// Hosts the FlyThroughOverlay control from the control template
        /// This control is requested in the TemplatePart at the top of this control
        /// The FlyThroughOverlay control is actually retrieved in OnApplyTemplate
        /// </summary>
        protected FlyThroughOverlay FlyThroughOverlay
        {
            get
            {
                return flyThroughOverlay;
            }
            set
            {
                flyThroughOverlay = value;
            }
        }

        /// <summary>
        /// Get and set the SpecialImageOverlay control (which is an ItemsControl)
        /// </summary>
        protected SpecialImageOverlay SpecialImageOverlay
        {
            get
            {
                return specialImageOverlay;
            }
            set
            {
                specialImageOverlay = value;
            }
        }

        /// <summary>
        /// Hosts the RootMultiScaleImage control from the control template
        /// This control is requested in the TemplatePart at the top of this control
        /// The RootMultiScaleImage control is actually retrieved in OnApplyTemplate
        /// </summary>
        protected MultiScaleImage RootMultiScaleImage
        {
            get
            {
                return msi;
            }
            set
            {
                if (msi != null)
                {
                    //remove any event handlers here as this control is about to be altered
                    msi.MotionFinished -= new RoutedEventHandler(msi_MotionFinished);
                }

                msi = value;

                msi.MotionFinished += new RoutedEventHandler(msi_MotionFinished);
            }
        }

        #endregion

        #region Properties

        /// <summary>
        /// This property wraps the DeepZoomSourceProperty DependencyProperty
        /// so that you can easily access it from C# code.
        /// Dependency properties are required for binding.        
        /// </summary>
        public string DeepZoomSource
        {
            get
            {
                var s = GetValue(DeepZoomSourceProperty);
                if (s != null)
                {
                    return s.ToString();
                }
                else
                {
                    return null;
                }
            }
            set
            {
                SetValue(DeepZoomSourceProperty, value);
            }
        }

        /// <summary>
        /// The image that is currently being viewed. It may be null if no image is being viewed
        /// </summary>
        public int? ViewedImage
        {
            get
            {
                return arranger.ViewedImage;
            }
        }

        /// <summary>
        /// Provides the FlyThrough hash to the UI so the user may copy and
        /// paste it to another browser / computer (i.e. deep linking)
        /// </summary>
        public string FlyThroughHash
        {
            get
            {
                return flyThroughManager.HashUrl;
            }
        }

        /// <summary>
        /// Allows the UI to bind the enabled state of the FlyThrough overlay
        /// </summary>
        public bool FlyThroughOverlayEnabled
        {
            get
            {
                return (bool)GetValue(FlyThroughOverlayEnabledProperty);
            }
            set
            {
                SetValue(FlyThroughOverlayEnabledProperty, value);
            }
        }

        /// <summary>
        /// The current filtered list of id's in the system
        /// </summary>
        public List<int> FilterIds
        {
            get
            {
                return GetValue(FilterIdsProperty) as List<int>;
            }
            set
            {
                SetValue(FilterIdsProperty, value);
            }
        }

        /// <summary>
        /// If the current viewed image has a context this will return it
        /// Contexts are passed in using ImageContextData below
        /// </summary>
        public object CurrentImageContext
        {
            get
            {
                if (ImageContextData != null && ViewedImage != null && ViewedImage.HasValue)
                {
                    if (ImageContextData.ContainsKey(ViewedImage.Value))
                    {
                        return ImageContextData[ViewedImage.Value];
                    }
                }
                return null;
            }
        }

        /// <summary>
        /// Sets the JS function that will be called back when ever the viewed image chagnes
        /// </summary>
        public string ImageViewedJavaScriptFunction
        {
            get
            {
                return GetValue(ImageViewedJavaScriptFunctionProperty) as string;
            }
            set
            {
                SetValue(ImageViewedJavaScriptFunctionProperty, value);
            }
        }

        /// <summary>
        /// The default time which a slide will be displayed before moving to the next during the slide show
        /// </summary>
        public int SlideshowTimeout
        {
            get
            {
                return (int)GetValue(SlideshowTimeoutProperty);
            }
            set
            {
                SetValue(SlideshowTimeoutProperty, value);
            }
        }

        /// <summary>
        /// Shows or hides the special image overlay which is a special overlay that can
        /// be displayed over each image in the collection. This works in unison with ImageContextData
        /// </summary>
        public bool SpecialImageOverlayEnabled
        {
            get
            {
                return (bool)GetValue(SpecialImageOverlayEnabledProperty);
            }
            set
            {
                SetValue(SpecialImageOverlayEnabledProperty, value);
            }
        }

        /// <summary>
        /// Image context data can be supplied for each image in the MSI sub collection
        /// The id is the index of the sub image (starting from 0) and the object will
        /// be made available as images are viewed and / or in the overlay
        /// </summary>
        public Dictionary<int, object> ImageContextData
        {
            get
            {
                return GetValue(ImageContextDataProperty) as Dictionary<int, object>;
            }
            set
            {
                SetValue(ImageContextDataProperty, value);
            }
        }

        /// <summary>
        /// Returns true if a # based flythrough has been passed in as part of the calling URL
        /// </summary>
        public bool HasUrlFlyThrough
        {
            get
            {
                if (flyThroughManager != null)
                {
                    return flyThroughManager.HasUrlFlyThrough;
                }
                else
                {
                    return false;
                }
            }
        }

      

        #endregion

        #region DependencyPropertyRegistration

        /// <summary>
        /// Construct the dependency properties. Some properties have PropertyMetdata which allows the firing
        /// of events and code when the property changes, handy to intercept binding updates and handle them 
        /// </summary>
        static SuperDeepZoom()
        {
            DeepZoomSourceProperty = DependencyProperty.Register("DeepZoomSource", typeof(string), typeof(SuperDeepZoom), new PropertyMetadata(null, (s, e) =>
            {
                //When the deep zoom source changes, we need to reset the control by calling init
                (s as SuperDeepZoom).init();
            }));

            FlyThroughOverlayEnabledProperty = DependencyProperty.Register("FlyThroughOverlayEnabled", typeof(bool), typeof(SuperDeepZoom), new PropertyMetadata(false, (s, e) =>
            {
                (s as SuperDeepZoom).toggleFlyThroughEnabled();
            }));

            SpecialImageOverlayEnabledProperty = DependencyProperty.Register("SpecialImageOverlayEnabled", typeof(bool), typeof(SuperDeepZoom), new PropertyMetadata(false, (s, e) =>
            {
                (s as SuperDeepZoom).toggleSpecialImageOverlayEnabled();
            }));

            FilterIdsProperty = DependencyProperty.Register("FilterIds", typeof(List<int>), typeof(SuperDeepZoom), new PropertyMetadata(null, (s, e) =>
            {
                (s as SuperDeepZoom).initFilterIds();
            }));

            ImageContextDataProperty = DependencyProperty.Register("ImageContextData", typeof(Dictionary<int, object>), typeof(SuperDeepZoom), new PropertyMetadata(null, (s, e) =>
            {
                (s as SuperDeepZoom).contextDataEvents(e.OldValue as Dictionary<int, object>, e.NewValue as Dictionary<int, object>);
                (s as SuperDeepZoom).prepSpecialOverlay();
            }));

            ImageViewedJavaScriptFunctionProperty = DependencyProperty.Register("ImageViewedJavaScriptFunction", typeof(string), typeof(SuperDeepZoom), null);

            SlideshowTimeoutProperty = DependencyProperty.Register("SlideshowTimeout", typeof(int), typeof(SuperDeepZoom), new PropertyMetadata(5000));
        }

        #endregion


        #region INotifyPropertyChanged Members

        void onPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
    }


}
